package org.tr.generator;

import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.IOException;

/**
 * This is the main class responsible to to change the byte code of the application to
 * make it thinks it run faster, This is done by changing time related calls to the JDK
 * speed sets constant sets the speed of the running application
 *
 * @author Lior Ben Zeev
 */
public class Generator {

    // Set how many times fast we want the application to run
    private static int speed=100;

    public static int getSpeed() {
        return speed;
    }

    public static void setSpeed(int speed) {
        Generator.speed = speed;
    }

    public static long calcNewInterval(long interval) {
        long newInterval = interval/speed;

        if (newInterval==0) {
            newInterval=1;
        }

        return newInterval;
    }

    private static Long startTime=System.currentTimeMillis();
    private static Long startNanoTime=System.nanoTime();

    public static byte[] getModifedClass(String className, byte[] origBytes) throws IOException {
        // Proxy classes starts with dollar character
        if (!className.startsWith("$") && !className.contains("$")) {
            try {
                ClassReader cr = new ClassReader(className);
                ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
                ReplaceStaticMethodClassAdapter cv = new ReplaceStaticMethodClassAdapter(cw);
                cr.accept(cv, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);

                if (cv.isClassChanged()) {
                    return cw.toByteArray();
                } else {
                    return origBytes;
                }
            } catch (IOException e) {
                System.out.println("Failed to load class: "+className);
                return origBytes;
            }
        } else {
            return origBytes;
        }
    }

    

    private static class ReplaceStaticMethodClassAdapter extends ClassAdapter{


        private boolean classChanged = false;

        public boolean isClassChanged() {
            return classChanged;
        }

        public void setClassChanged(boolean classChanged) {
            this.classChanged = classChanged;
        }

        public ReplaceStaticMethodClassAdapter(ClassVisitor classVisitor) {
            super(classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
            return new ReplaceStaticMethodAdapter(super.visitMethod(i, s, s1, s2, strings));
        }

        private class ReplaceStaticMethodAdapter extends MethodAdapter{

            private boolean ldc=false;
            private Object ldcObject = null;

            public ReplaceStaticMethodAdapter(MethodVisitor methodVisitor) {
                super(methodVisitor);
            }

            /////////////////////////////////////////////////////////////////////////////////////////////
            // Support database select time -
            // in Prepared statement if sql contains getdate() or SYSDATE, change it to '1/5/2001'
            // according to the current virtual time
            //////////////////////////////////////////////////////////////////////////////////////////////


            @Override
            public void visitMethodInsn(int i, String s, String s1, String s2) {

                //System.out.println("XXX  s: "+s + " s1: "+s1 + " s2: "+s2);
                if(i == Opcodes.INVOKESTATIC &&  "java/lang/System".equals(s) && "currentTimeMillis".equals(s1) && "()J".equals(s2)){
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/tr/generator/Generator", "myTime", "()J");
                    setClassChanged(true);
                } else if (i == Opcodes.INVOKESTATIC &&  "java/lang/Thread".equals(s) && "sleep".equals(s1) && "(J)V".equals(s2)){
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/tr/generator/Generator", "timeRunnerSleep", "(J)V");
                    setClassChanged(true);
                } else if (i == Opcodes.INVOKESTATIC &&  "java/lang/Thread".equals(s) && "sleep".equals(s1) && "(JI)V".equals(s2)){
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/tr/generator/Generator", "timeRunnerSleep", "(JI)V");
                    setClassChanged(true);

                } else if (i == Opcodes.INVOKEVIRTUAL && "java/util/Timer".equals(s) && "scheduleAtFixedRate".equals(s1) && "(Ljava/util/TimerTask;JJ)V".equals(s2)) {
                    System.out.println("i:= " + i);
                    System.out.println("visitMethodInsn s: "+s + " s1: "+s1 + " s2: "+s2);
                    super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/tr/generator/TimeRunnerTimer", "scheduleAtFixedRate", "(Ljava/util/TimerTask;JJ)V");
                    System.out.println("After Visit");
                } else if ("java/util/Timer".equals(s)) { // && !("<init>".equals(s1))) {
                    //System.out.println("AAA  s: "+s + " s1: "+s1 + " s2: "+s2);
                    super.visitMethodInsn(i, "org/tr/generator/TimeRunnerTimer", s1, s2);
                    //System.out.println("BBB  s: "+s + " s1: "+s1 + " s2: "+s2);
                    setClassChanged(true);
                } else if ("java/util/Date".equals(s)) { // && !("<init>".equals(s1))) {
                    //System.out.println("AAA  s: "+s + " s1: "+s1 + " s2: "+s2);
                    super.visitMethodInsn(i, "org/tr/generator/TimeRunnerDate", s1, s2);
                    //System.out.println("BBB  s: "+s + " s1: "+s1 + " s2: "+s2);
                    setClassChanged(true);
                } else {
                    super.visitMethodInsn(i, s, s1, s2);
                }
            }

            @Override
            public void visitTypeInsn(int i, String s) {
                //System.out.println("i: "+i + " s: "+s);
                if (s.equals("java/util/Date")) {
                    //System.out.println("org/tr/generator/Date2");
                    super.visitTypeInsn(i, "org/tr/generator/TimeRunnerDate");
                    setClassChanged(true);
                } else if (s.equals("java/util/Timer")) {
                    System.out.println("org/tr/generator/TimeRunnerTimer");
                    super.visitTypeInsn(i, "org/tr/generator/TimeRunnerTimer");
                    setClassChanged(true);
                } else {
                    //System.out.println("ELSE");
                    super.visitTypeInsn(i, s);
                    //classChanged=true;
                }
            }

            @Override
            public void visitVarInsn(int i, int i1) {
                ldc=false;
                super.visitVarInsn(i, i1);
            }

            @Override
            public void visitLdcInsn(Object o) {
                ldc=true;
                ldcObject = o;
                super.visitLdcInsn(o);
            }
        }
    }

    //Test Ny Nano Time
    public static long myTime(){
        //System.out.println("orig: "+System.currentTimeMillis());
        long fake = (System.currentTimeMillis()-startTime)*getSpeed()+startTime+1000L*60L;
        //System.out.println("fake: "+ fake);
        return fake;
    }

        public static long myNanoTime(){
            //System.out.println("orig: "+System.currentTimeMillis());
            long fake = (System.nanoTime()-startNanoTime)*getSpeed()+startNanoTime+1000L*60L;
            //System.out.println("fake: "+ fake);
            return fake;
        }


    public static void timeRunnerSleep(long milliseconds) throws InterruptedException {
        Thread.sleep(milliseconds/getSpeed());
    }
    
    public static void timeRunnerSleep(long milliseconds, int nanos) throws InterruptedException {
        Thread.sleep(milliseconds/getSpeed(), nanos/getSpeed());
    }


}

